/*
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package tw.idv.gasolin.pycontw2012.ui;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.TimeZone;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.database.Cursor;
import android.graphics.Rect;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.BaseColumns;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

import tw.idv.gasolin.pycontw2012.R;
import tw.idv.gasolin.pycontw2012.provider.CoscupContract;
import tw.idv.gasolin.pycontw2012.util.AnalyticsUtils;
import tw.idv.gasolin.pycontw2012.util.MotionEventUtils;
import tw.idv.gasolin.pycontw2012.util.NotifyingAsyncQueryHandler;
import tw.idv.gasolin.pycontw2012.util.ParserUtils;
import tw.idv.gasolin.pycontw2012.util.UIUtils;
import tw.idv.gasolin.pycontw2012.widget.BlockView;
import tw.idv.gasolin.pycontw2012.widget.BlocksLayout;
import tw.idv.gasolin.pycontw2012.widget.ObservableScrollView;
import tw.idv.gasolin.pycontw2012.widget.Workspace;

/**
 * Shows a horizontally-pageable calendar of conference days. Horizontaly paging
 * is achieved using {@link Workspace}, and the primary UI classes for rendering
 * the calendar are {@link tw.idv.gasolin.pycontw2012.ui.widget.TimeRulerView},
 * {@link BlocksLayout}, and {@link BlockView}.
 */
public class ScheduleFragment extends Fragment implements
    NotifyingAsyncQueryHandler.AsyncQueryListener,
    ObservableScrollView.OnScrollListener, View.OnClickListener {

    private static final String LOG_TAG = ScheduleFragment.class.getSimpleName();

    /**
     * Flags used with {@link android.text.format.DateUtils#formatDateRange}.
     */
    private static final int TIME_FLAGS = DateUtils.FORMAT_SHOW_DATE
        | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY;

    private static final long FIRST_DAY_START = ParserUtils.parseTime("2012-06-09T00:00:00.000+08:00");
    private static final long SECOND_DAY_START = ParserUtils.parseTime("2012-06-10T00:00:00.000+08:00");

    private static final int DISABLED_BLOCK_ALPHA = 100;

    private static final HashMap<String, Integer> sTypeColumnMap = buildTypeColumnMap();

    // TODO: show blocks that don't fall into columns at the bottom

    public static final String EXTRA_TIME_START = "tw.idv.gasolin.pycontw2012.extra.TIME_START";
    public static final String EXTRA_TIME_END = "tw.idv.gasolin.pycontw2012.extra.TIME_END";

    private NotifyingAsyncQueryHandler mHandler;

    private Workspace mWorkspace;
    private TextView mTitle;
    private int mTitleCurrentDayIndex = -1;
    private View mLeftIndicator;
    private View mRightIndicator;

    /**
     * A helper class containing object references related to a particular day
     * in the schedule.
     */
    private class Day {
        private ViewGroup rootView;
        private ObservableScrollView scrollView;
        private View nowView;
        private BlocksLayout blocksView;

        private int index = -1;
        private String label = null;
        private Uri blocksUri = null;
        private long timeStart = -1;
        private long timeEnd = -1;
    }

    private List<Day> mDays = new ArrayList<Day>();

    private static HashMap<String, Integer> buildTypeColumnMap() {
        final HashMap<String, Integer> map = new HashMap<String, Integer>();
        map.put(ParserUtils.BLOCK_TYPE_FOOD, 0);
        map.put(ParserUtils.BLOCK_TYPE_SESSION, 1);
        return map;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler = new NotifyingAsyncQueryHandler(
            getActivity().getContentResolver(),
            this);
        setHasOptionsMenu(true);
        AnalyticsUtils.getInstance(getActivity())
            .trackPageView("/Schedule");
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
        ViewGroup root = (ViewGroup) inflater.inflate(
            R.layout.fragment_schedule, null);

        mWorkspace = (Workspace) root.findViewById(R.id.workspace);

        mTitle = (TextView) root.findViewById(R.id.block_title);

        mLeftIndicator = root.findViewById(R.id.indicator_left);
        mLeftIndicator.setOnTouchListener(new View.OnTouchListener() {
            public boolean onTouch(View view, MotionEvent motionEvent) {
                if ( ( motionEvent.getAction() & MotionEventUtils.ACTION_MASK ) == MotionEvent.ACTION_DOWN ) {
                    mWorkspace.scrollLeft();
                    return true;
                }
                return false;
            }
        });
        mLeftIndicator.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                mWorkspace.scrollLeft();
            }
        });

        mRightIndicator = root.findViewById(R.id.indicator_right);
        mRightIndicator.setOnTouchListener(new View.OnTouchListener() {
            public boolean onTouch(View view, MotionEvent motionEvent) {
                if ( ( motionEvent.getAction() & MotionEventUtils.ACTION_MASK ) == MotionEvent.ACTION_DOWN ) {
                    mWorkspace.scrollRight();
                    return true;
                }
                return false;
            }
        });
        mRightIndicator.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                mWorkspace.scrollRight();
            }
        });

        setupDay(inflater, FIRST_DAY_START);
        setupDay(inflater, SECOND_DAY_START);

        updateWorkspaceHeader(0);
        mWorkspace.setOnScrollListener(new Workspace.OnScrollListener() {
            public void onScroll(float screenFraction) {
                updateWorkspaceHeader(Math.round(screenFraction));
            }
        }, true);

        return root;
    }

    public void updateWorkspaceHeader(int dayIndex) {
        if ( mTitleCurrentDayIndex == dayIndex ) {
            return;
        }

        mTitleCurrentDayIndex = dayIndex;
        Day day = mDays.get(dayIndex);
        mTitle.setText(day.label);

        mLeftIndicator.setVisibility( ( dayIndex != 0 ) ? View.VISIBLE
            : View.INVISIBLE);
        mRightIndicator.setVisibility( ( dayIndex < mDays.size() - 1 )
            ? View.VISIBLE : View.INVISIBLE);
    }

    private void setupDay(LayoutInflater inflater, long startMillis) {
        Day day = new Day();

        // Setup data
        day.index = mDays.size();
        day.timeStart = startMillis;
        day.timeEnd = startMillis + DateUtils.DAY_IN_MILLIS;
        day.blocksUri = CoscupContract.Blocks.buildBlocksBetweenDirUri(
            day.timeStart, day.timeEnd);

        // Setup views
        day.rootView = (ViewGroup) inflater.inflate(R.layout.blocks_content,
            null);

        day.scrollView = (ObservableScrollView) day.rootView.findViewById(R.id.blocks_scroll);
        day.scrollView.setOnScrollListener(this);

        day.blocksView = (BlocksLayout) day.rootView.findViewById(R.id.blocks);
        day.nowView = day.rootView.findViewById(R.id.blocks_now);

        day.blocksView.setDrawingCacheEnabled(true);
        day.blocksView.setAlwaysDrawnWithCacheEnabled(true);

        TimeZone.setDefault(UIUtils.CONFERENCE_TIME_ZONE);
        day.label = DateUtils.formatDateTime(getActivity(), startMillis,
            TIME_FLAGS);

        mWorkspace.addView(day.rootView);
        mDays.add(day);
    }

    @Override
    public void onResume() {
        super.onResume();

        // Since we build our views manually instead of using an adapter, we
        // need to manually requery every time launched.
        requery();

        getActivity().getContentResolver()
            .registerContentObserver(CoscupContract.Sessions.CONTENT_URI, true,
                mSessionChangesObserver);

        // Start listening for time updates to adjust "now" bar. TIME_TICK is
        // triggered once per minute, which is how we move the bar over time.
        final IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_TIME_TICK);
        filter.addAction(Intent.ACTION_TIME_CHANGED);
        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
        getActivity().registerReceiver(mReceiver, filter, null, new Handler());
    }

    private void requery() {
        for ( Day day : mDays ) {
            mHandler.startQuery(0, day, day.blocksUri, BlocksQuery.PROJECTION,
                null, null, CoscupContract.Blocks.DEFAULT_SORT);
        }
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        getActivity().runOnUiThread(new Runnable() {
            public void run() {
                updateNowView(true);
            }
        });
    }

    @Override
    public void onPause() {
        super.onPause();
        getActivity().unregisterReceiver(mReceiver);
        getActivity().getContentResolver()
            .unregisterContentObserver(mSessionChangesObserver);
    }

    /**
     * {@inheritDoc}
     */
    public void onQueryComplete(int token, Object cookie, Cursor cursor) {
        if ( getActivity() == null ) {
            return;
        }

        Day day = (Day) cookie;

        // Clear out any existing sessions before inserting again
        day.blocksView.removeAllBlocks();

        ArrayList<BlockView> bvs = new ArrayList<BlockView>();

        try {
            while ( cursor.moveToNext() ) {
                final String type = cursor.getString(BlocksQuery.BLOCK_TYPE);
                final Integer column = sTypeColumnMap.get(type);
                // TODO: place random blocks at bottom of entire layout
                if ( column == null ) {
                    continue;
                }

                final String blockId = cursor.getString(BlocksQuery.BLOCK_ID);
                String title = cursor.getString(BlocksQuery.BLOCK_TITLE);
                final long start = cursor.getLong(BlocksQuery.BLOCK_START);
                final long end = cursor.getLong(BlocksQuery.BLOCK_END);
                final boolean containsStarred = cursor.getInt(BlocksQuery.CONTAINS_STARRED) != 0;

                if ( TextUtils.equals(title,
                    ParserUtils.BLOCK_TITLE_BREAKOUT_SESSIONS) ) {
                    title = getString(R.string.block_title_breakout_sessions);
                }

                final BlockView blockView = new BlockView(
                    getActivity(),
                    blockId,
                    title,
                    start,
                    end,
                    containsStarred,
                    column);

                final int sessionsCount = cursor.getInt(BlocksQuery.SESSIONS_COUNT);
                if ( sessionsCount > 0 ) {
                    blockView.setOnClickListener(this);
                } else {
                    blockView.setFocusable(false);
                    blockView.setEnabled(false);
                    LayerDrawable buttonDrawable = (LayerDrawable) blockView.getBackground();
                    buttonDrawable.getDrawable(0)
                        .setAlpha(DISABLED_BLOCK_ALPHA);
                    buttonDrawable.getDrawable(2)
                        .setAlpha(DISABLED_BLOCK_ALPHA);
                }

                bvs.add(blockView);
            }
        } finally {
            cursor.close();
        }

        ArrayList<BlockView> groupList = new ArrayList<BlockView>();
        for ( BlockView curr : bvs ) {
            int groupSize = groupList.size();
            BlockView prev = null;
            if ( groupSize > 0 ) {
                for ( int i = groupSize - 1; i >= 0; i-- ) {
                    BlockView bv = groupList.get(i);
                    if ( prev == null ) {
                        prev = bv;
                    } else {
                        if ( bv.getEndTime() > prev.getEndTime()
                            || ( bv.getEndTime() == prev.getEndTime() && bv.getStartTime() < prev.getStartTime() ) ) {
                            prev = bv;
                        }
                    }
                }
            }

            if ( BlockView.areOverlapping(prev, curr) ) {
                // there is an overlap

                final int prevMaxSubCols = prev.getMaxSubColumns();
                int subcol = prevMaxSubCols;

                for ( int i = 0; i < groupSize; i++ ) {
                    BlockView thisBv = groupList.get(i);
                    BlockView nextBv = ( i + 1 == groupSize ) ? null
                        : groupList.get(i + 1);
                    if ( nextBv == null
                        || thisBv.getSubColumn() != nextBv.getSubColumn() ) {
                        if ( !BlockView.areOverlapping(curr, thisBv) ) {
                            subcol = thisBv.getSubColumn();
                            break;
                        }
                    }
                }

                curr.setSubColumn(subcol);
                if ( subcol < prevMaxSubCols ) {
                    curr.setMaxSubColumns(prevMaxSubCols);
                } else {
                    curr.setMaxSubColumns(prevMaxSubCols + 1);
                    for ( BlockView bv : groupList ) {
                        bv.setMaxSubColumns(prevMaxSubCols + 1);
                    }
                }

            } else {
                // no overlap
                curr.setSubColumn(0);
                curr.setMaxSubColumns(1);
                groupList.clear();
                groupSize = 0;
            }

            int insertAt = 0;
            for ( int i = 0; i < groupSize; i++ ) {
                BlockView bv = groupList.get(i);
                if ( bv.getSubColumn() > curr.getSubColumn() ) {
                    insertAt = i;
                    break;
                }
            }
            groupList.add(insertAt, curr);
        }

        for ( BlockView blockView : bvs ) {
            day.blocksView.addBlock(blockView);
        }
    }

    /** {@inheritDoc} */
    public void onClick(View view) {
        if ( view instanceof BlockView ) {
            String title = ( (BlockView) view ).getText()
                .toString();
            AnalyticsUtils.getInstance(getActivity())
                .trackEvent("Schedule", "Session Click", title, 0);
            final String blockId = ( (BlockView) view ).getBlockId();
            final Uri sessionsUri = CoscupContract.Blocks.buildSessionsUri(blockId);

            final Intent intent = new Intent(Intent.ACTION_VIEW, sessionsUri);
            intent.putExtra(SessionsFragment.EXTRA_SCHEDULE_TIME_STRING,
                ( (BlockView) view ).getBlockTimeString());
            ( (BaseActivity) getActivity() ).openActivityOrFragment(intent);
        }
    }

    /**
     * Update position and visibility of "now" view.
     */
    private boolean updateNowView(boolean forceScroll) {
        final long now = UIUtils.getCurrentTime(getActivity());

        Day nowDay = null; // effectively Day corresponding to today
        for ( Day day : mDays ) {
            if ( now >= day.timeStart && now <= day.timeEnd ) {
                nowDay = day;
                day.nowView.setVisibility(View.VISIBLE);
            } else {
                day.nowView.setVisibility(View.GONE);
            }
        }

        if ( nowDay != null && forceScroll ) {
            // Scroll to show "now" in center
            mWorkspace.setCurrentScreen(nowDay.index);
            final int offset = nowDay.scrollView.getHeight() / 2;
            nowDay.nowView.requestRectangleOnScreen(new Rect(
                0,
                offset,
                0,
                offset), true);
            nowDay.blocksView.requestLayout();
            return true;
        }

        return false;
    }

    public void onScrollChanged(ObservableScrollView view) {
        // Keep each day view at the same vertical scroll offset.
        final int scrollY = view.getScrollY();
        for ( Day day : mDays ) {
            if ( day.scrollView != view ) {
                day.scrollView.scrollTo(0, scrollY);
            }
        }
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.schedule_menu_items, menu);
        super.onCreateOptionsMenu(menu, inflater);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if ( item.getItemId() == R.id.menu_now ) {
            if ( !updateNowView(true) ) {
                Toast.makeText(getActivity(), R.string.toast_now_not_visible,
                    Toast.LENGTH_SHORT)
                    .show();
            }
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private ContentObserver mSessionChangesObserver = new ContentObserver(
        new Handler()) {
        @Override
        public void onChange(boolean selfChange) {
            requery();
        }
    };

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d(LOG_TAG, "onReceive time update");
            updateNowView(false);
        }
    };

    private interface BlocksQuery {
        String[] PROJECTION = { BaseColumns._ID,
            CoscupContract.Blocks.BLOCK_ID, CoscupContract.Blocks.BLOCK_TITLE,
            CoscupContract.Blocks.BLOCK_START, CoscupContract.Blocks.BLOCK_END,
            CoscupContract.Blocks.BLOCK_TYPE,
            CoscupContract.Blocks.SESSIONS_COUNT,
            CoscupContract.Blocks.CONTAINS_STARRED, };

        int _ID = 0;
        int BLOCK_ID = 1;
        int BLOCK_TITLE = 2;
        int BLOCK_START = 3;
        int BLOCK_END = 4;
        int BLOCK_TYPE = 5;
        int SESSIONS_COUNT = 6;
        int CONTAINS_STARRED = 7;
    }
}
